package edu.geo.geomdomain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;

import edu.geo.containers.geom.impl.GeometryFactory;
import edu.geo.containers.geom.impl.LineSegment;
import edu.geo.containers.geom.impl.Point;
import edu.geo.containers.geomdomain.GeometricDomain;
import edu.geo.containers.geomdomain.GeometricDomainFault;
import edu.geo.containers.geomdomain.Grid;
import edu.geo.exception.GeoInfoException;


/**
 * 
 * @author alejandro.sanchez
 * 
 * Esta clase implementa el algoritmo de Greene-Yao para ajuste de dominios geométricos. El ajuste consiste en
 * garantizar que las cuatro condiciones de clausura (@see GeometricDomainValidator) se cumplen. Se implementan 
 * cuatro métodos que corrijen el dominio geométrico pasado como parámetro a la única función publica de la 
 * clase {@link #adjust(GeometricDomain)}:
 * 
 *  Al no necesitarse un estado de persistencia de los objetos durante la ejecución del algoritmo, se dejan todos
 *  los métodos de la clase como estáticos, de esta manera no es necesario instanciar un objeto de la clase para
 *  ajustar el dominio geométrico pasado como parámetro al único método público {@link #adjust(GeometricDomain)}
 *
 */
public class GeometricDomainFixer extends GeometricDomainValidator implements
		Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -565799640594878663L;

	private static final Log LOGGER = LogFactory
			.getLog(GeometricDomainFixer.class);

	/**
	 * Ajusta el dominio geométrico pasado como parámetro en un nuevo dominio geométrico al que se le aplican todas
	 * los ajustes necesarios para cumplir las condiciones de clausura que se requieren para que sea un dominio válido.
	 *  
	 * 
	 * @param geomDomain
	 * @return
	 * @throws GeoInfoException
	 */
	public static GeometricDomain adjust(GeometricDomain geomDomain)
			throws GeoInfoException {
		GeometricDomain fixGeometricDomain = new GeometricDomain();
		fixGeometricDomain.setId(geomDomain.getId());
		fixGeometricDomain.setUser(geomDomain.getUser());
		fixGeometricDomain.setGrid(geomDomain.getGrid());
		if (geomDomain.getLineSegments() != null
				&& !geomDomain.getLineSegments().isEmpty()) {
			fixGeometricDomain.setLineSegments(new ArrayList<LineSegment>(
					geomDomain.getLineSegments()));
		} else {
			fixGeometricDomain.setLineSegments(new ArrayList<LineSegment>());
		}
		if (geomDomain.getPoints() == null) {
			fixGeometricDomain.setPoints(new ArrayList<Point>());
		} else {
			fixGeometricDomain.setPoints(new ArrayList<Point>(geomDomain
					.getPoints()));
		}
		adjustOutsidePoints(fixGeometricDomain);
		adjustEndLineSegments(fixGeometricDomain);
		adjustIncidentPoints(fixGeometricDomain);
		adjustIntersectionPoints(fixGeometricDomain);
		// Para el dominio geométrico los puntos y las líneas se evaluarán con
		// las condiciones de clausura
		// de un dominio geométrico.
		return fixGeometricDomain;
	}

	/**
	 * Remueve todos los puntos del dominio geométrico que no hacen parte de la
	 * grilla.
	 * 
	 * @param geomDomain
	 * @param fixGeomDomain
	 * @throws GeoInfoException
	 */
	private static void adjustOutsidePoints(GeometricDomain fixGeomDomain)
			throws GeoInfoException {
		List<GeometricDomainFault> faults = new ArrayList<GeometricDomainFault>();
		validateGeometricDomainPoints(fixGeomDomain, faults);
		List<Point> points = fixGeomDomain.getPoints();
		// Si los puntos no están dentro de la grilla entonces se eliminan del
		// dominio geométrico
		for (GeometricDomainFault fault : faults) {
			points.remove(fault.getLocation());
		}
		fixGeomDomain.setPoints(points);
	}

	private static void adjustEndLineSegments(GeometricDomain fixGeomDomain)
			throws GeoInfoException {

		List<Point> points = fixGeomDomain.getPoints();
		List<LineSegment> lineSegments = fixGeomDomain.getLineSegments();
		List<LineSegment> newLineSegments = new ArrayList<LineSegment>(
				fixGeomDomain.getLineSegments());
		Grid grid = fixGeomDomain.getGrid();
		// GeometryCollection geomCollection = getGridPoints(grid);
		for (LineSegment line : lineSegments) {
			if (!verifyPointOnGrid(line.getBeginNode(), grid)
					|| !verifyPointOnGrid(line.getEndNode(), grid)) {
				newLineSegments.remove(line);
			} else {
				if (!containsByGeometry(line.getBeginNode(), points) )
				{
					points.add(line.getBeginNode());
				}
				if (!containsByGeometry(line.getEndNode(), points)) {
					points.add(line.getEndNode());
				}
			}
		}
		fixGeomDomain.setPoints(points);
		fixGeomDomain.setLineSegments(newLineSegments);
	}

	private static void adjustIncidentPoints(GeometricDomain fixGeomDomain)
			throws GeoInfoException {
		// List<Point> points = fixGeomDomain.getPoints();
		List<LineSegment> lineSegments = fixGeomDomain.getLineSegments();
		List<LineSegment> newLineSegments = new ArrayList<LineSegment>();
		List<Point> newPoints;
		List<Geometry> intPoints;
		for (LineSegment line : lineSegments) {
			newPoints = new ArrayList<Point>(fixGeomDomain.getPoints());
			List<Point> points = fixGeomDomain.getPoints();
			intPoints = new ArrayList<Geometry>();
			for (Point point : points) {
				if (line.intersects(point)) {
					Geometry intersection = line.intersection(point);
					intPoints.add(intersection);
				}
			}
			if (intPoints.size() > 2) {
				Geometry[] aGeoms = new Geometry[intPoints.size()];
				for (int i = 0; i < intPoints.size(); ++i) {
					aGeoms[i] = intPoints.get(i);
				}
				GeometryCollection geomCollection = new com.vividsolutions.jts.geom.GeometryFactory()
						.createGeometryCollection(aGeoms);
				// Se ordena la lista de puntos incidentes para crear las nuevas
				// polilineas
				Geometry sortGeom = order(line.getBeginNode(), geomCollection);
				// Se crean los nuevos segmentos de línea
				for (int i = 0; i < sortGeom.getNumGeometries() - 1; ++i) {
					Point startPoint = GeometryFactory.createPoint(sortGeom
							.getGeometryN(i).getCoordinates());
					Point endPoint = GeometryFactory.createPoint(sortGeom
							.getGeometryN(i + 1).getCoordinates());
					if (i == 0) {
						startPoint.setId(line.getBeginNode().getId());
					} else if (i == sortGeom.getNumGeometries() - 1) {
						endPoint.setId(line.getEndNode().getId());
					}
					if (!containsByGeometry(startPoint, points)) {
						newPoints.add(startPoint);
					}
					if (!containsByGeometry(endPoint, points)) {
						newPoints.add(endPoint);
					}
					newLineSegments.add(GeometryFactory.createLineSegment(
							startPoint, endPoint));
				}
			}
			// Si no hay puntos incidentes entonces se deja el mismo segmento de
			// línea
			else {
				newLineSegments.add(line);
			}
			fixGeomDomain.setPoints(newPoints);
		}
		fixGeomDomain.setLineSegments(newLineSegments);
		LOGGER.info(fixGeomDomain.getPoints().toString());
		LOGGER.info(fixGeomDomain.getLineSegments().toString());

	}

	private static void adjustIntersectionPoints(GeometricDomain fixGeomDomain) {
		List<LineSegment> newLineSegments = new ArrayList<LineSegment>();
		List<LineSegment> lineSegments1 = fixGeomDomain.getLineSegments();
		List<LineSegment> lineSegments2 = fixGeomDomain.getLineSegments();
		List<Point> points = fixGeomDomain.getPoints();
		List<Point> newPoints = points;
		Grid grid = fixGeomDomain.getGrid();
		GeometryCollection gridPoints = getGridPoints(grid);
		// Se verifican cada par de líneas en los que hay intersección, teniendo
		// \
		// en cuenta que al volver a evaluar, los 4 segmentos de línea
		// existentes harán parte de la evalución.
		if (lineSegments1 != null && !lineSegments1.isEmpty()) {
			if (points == null) {
				points = new ArrayList<Point>();
			}
			for (LineSegment lineSegment : lineSegments1) {
				boolean intersects = false;
				for (LineSegment lineSegment2 : lineSegments2) {
					if (!lineSegment.equals(lineSegment2)) {
						Geometry intersection = GeometryFactory.createPoint((lineSegment.intersection(lineSegment2)).getCoordinates());						
						if (lineSegment.intersection(lineSegment2) != null
								&& intersection.getCoordinates().length > 0
								&& lineSegment.intersection(lineSegment2)
										.toText().contains("POINT")
								&& !(intersection.getCoordinate()
										.equals(lineSegment.getBeginNode()
												.getCoordinate()))
								&& !(intersection.getCoordinate()
										.equals(lineSegment.getEndNode()
												.getCoordinate()))) {
							intersects = true;
							newPoints = new ArrayList<Point>(
									fixGeomDomain.getPoints());
							boolean exists = false;
							Point intPoint = GeometryFactory
									.createPoint((com.vividsolutions.jts.geom.Point) lineSegment
											.intersection(lineSegment2));
							for (Point point : points) {
								if (point.getCoordinate().equals(
										intPoint.getCoordinate())) {
									exists = true;
								}
							}

							if (!exists) {

								// Si el punto corresponde a la grilla se agrega
								// al conjunto de puntos (esto no será necesario
								// si ya se han evaluado las condiciones
								// preliminares de validación, condicines de
								// clousura 1,2 y 3)
								if (verifyPointOnGrid(intPoint, grid)) {
									newPoints.add(intPoint);
								}
								// Se busca el punto más cercano a la grilla y
								// se elige como punto de corte para generar los
								// cuatro nuevos segmentos de línea, definidos a
								// partir de los 2 segmentos de línea analizados
								// y el punto de intersección
								else {
									// Obtener punto más cercano de la grilla
									GeometryCollection geomCollection = (GeometryCollection) order(
											intPoint, gridPoints);
									// El primero de la lista es el punto de
									// intersección más cercano
									intPoint = GeometryFactory
											.createPoint(geomCollection
													.getGeometryN(0)
													.getCoordinates());
									newPoints.add(intPoint);
								}

							}
							// Generar los nuevos 4 segmentos de línea y
							// tenerlos en cuenta para seguir evaluando si
							// hay que discretizar más puntos
							// Segmento de línea 1.1
							LineSegment lineSegment11 = GeometryFactory
									.createLineSegment(
											lineSegment.getBeginNode(),
											intPoint);
							// Segmento de línea 1.2
							LineSegment lineSegment12 = GeometryFactory
									.createLineSegment(intPoint,
											lineSegment.getEndNode());
							// Segmento de línea 2.1
							LineSegment lineSegment21 = GeometryFactory
									.createLineSegment(
											lineSegment2.getBeginNode(),
											intPoint);
							// Segmento de línea 2.2
							LineSegment lineSegment22 = GeometryFactory
									.createLineSegment(intPoint,
											lineSegment2.getEndNode());
							// Agregar los segmentos de línea nuevos
							if (!newLineSegments.contains(lineSegment11)) newLineSegments.add(lineSegment11);
							if (!newLineSegments.contains(lineSegment12)) newLineSegments.add(lineSegment12);
							if (!newLineSegments.contains(lineSegment21)) newLineSegments.add(lineSegment21);
							if (!newLineSegments.contains(lineSegment22)) newLineSegments.add(lineSegment22);
							fixGeomDomain.setPoints(newPoints);
							points = newPoints;
							fixGeomDomain.setLineSegments(newLineSegments);
							// Como hay cambios en la geometría entonces es
							// necesario llamar nuevamente a la función de
							// ajuste de líneas intersectadas
							adjustIntersectionPoints(fixGeomDomain);
						}
					}
				}
				if (!intersects) {
					if (!newLineSegments.contains(lineSegment))
					{
						newLineSegments.add(lineSegment);
					}
				}
			}
			fixGeomDomain.setLineSegments(newLineSegments);
			fixGeomDomain.setPoints(newPoints);
		}

	}

	private static Geometry order(Point beginNode, Geometry geometryCollection) {
		HashMap<Double, Geometry> distGeom = new LinkedHashMap<Double, Geometry>();
		for (int i = 0; i < geometryCollection.getNumGeometries(); ++i) {
			Double distance = new Double(beginNode.distance(geometryCollection
					.getGeometryN(i)));
			distGeom.put(distance, geometryCollection.getGeometryN(i));
		}
		Map<Double, Geometry> sortGeoms = sortByComparator(distGeom);
		Set<Double> keySet = sortGeoms.keySet();
		Geometry[] aGeoms = new Geometry[keySet.size()];
		int i = 0;
		for (Double dist : keySet) {
			aGeoms[i] = sortGeoms.get(dist);
			++i;
			LOGGER.info(dist + "  " + sortGeoms.get(dist));
		}
		GeometryCollection geomCollection = new com.vividsolutions.jts.geom.GeometryFactory()
				.createGeometryCollection(aGeoms);
		return geomCollection;
	}

	private static Map<Double, Geometry> sortByComparator(
			Map<Double, Geometry> unsortMap) {
		List<Double> sortDistances = new LinkedList<Double>(unsortMap.keySet());
		// sort list based on comparator
		Collections.sort(sortDistances, new Comparator<Double>() {
			@Override
			public int compare(Double o1, Double o2) {
				// TODO Auto-generated method stub
				return o1.compareTo(o2);
			}
		});
		// put sorted list into map again
		Map<Double, Geometry> sortedMap = new LinkedHashMap<Double, Geometry>();
		for (Double dist : sortDistances) {
			sortedMap.put(dist, unsortMap.get(dist));
		}
		return sortedMap;
	}

	private static GeometryCollection getGridPoints(Grid grid) {
		List<Point> points = new ArrayList<Point>();
		int originX = (int) grid.getOrigin().getX();
		int originY = (int) grid.getOrigin().getY();
		int intSize = grid.getIntSize();
		int minX = originX
				- ((int) Math.floor(grid.getNumPointsX() / 2) * intSize);
		int maxX = originX
				+ ((int) Math.ceil(grid.getNumPointsX() / 2) * intSize);
		int minY = originY
				- ((int) Math.floor(grid.getNumPointsY() / 2) * intSize);
		int maxY = originY
				+ ((int) Math.ceil(grid.getNumPointsY() / 2) * intSize);
		for (int i = minX; i <= maxX; ++i) {
			for (int j = minY; j <= maxY; ++j) {
				points.add(GeometryFactory.createPoint(i, j));
			}
		}
		Geometry[] aGeoms = new Geometry[points.size()];
		for (int i = 0; i < points.size(); ++i) {
			aGeoms[i] = (points.get(i));
		}
		GeometryCollection geomCollection = new com.vividsolutions.jts.geom.GeometryFactory()
				.createGeometryCollection(aGeoms);
		return geomCollection;
	}
	
}
